import { AObject } from "./AObject";
import type { AZone } from "./AZone";
import { Agent } from "./Agent";
import * as Debug from "./Debug";
import type { DevtoolsFormatterItem } from "./DebugFormatters";
import { U } from "./Util";
import { World } from "./WorldBase";

const SYNTAX_NODE_TYPES = [
    ['text', 'TEXT'],
    ['subject', 'S'],
    ['predicative', 'P'],
    // ['verb', 'V'],
    ['auxiliaryVerb', 'AUX_V'],
    ['preposition', 'PREP'],
    ['postposition', 'POSTP'],
    ['object', 'O'],
    ['indirectObject', 'IO'],
    ['nounPhraseList', 'LIST'],
    ['adverbial', 'ADV'],
    ['complement', 'COM'],
    ['coordinatingPredicatives', 'COORD_PREDS'],
    ['clause', 'CL']
] as const;

const SYNTAX_SPECIAL_TYPES = [
    'plainText', 
    'signifier',
    'verb',
    'grouped',
    'or',
    'any'
] as const;

type SyntaxElementType = 
    typeof SYNTAX_NODE_TYPES[number][0] | typeof SYNTAX_SPECIAL_TYPES[number];

type PlainText = string;
export type Referent = Agent | AObject;
export type SyntaxNode = {
    type: typeof SYNTAX_NODE_TYPES[number][0], 
    children: SyntaxElement[]
};

export class Signifier {
    type = 'signifier' as const;
    forceIndefiniteArticle = false;

    constructor(
        public object: Referent, 
        public form: string = '') {}

    static forceIndefinite(object: AObject, form: string = '') {
        let sig = new Signifier(object, form);
        sig.forceIndefiniteArticle = true;
        return sig;
    }

    get debugString() {
        if (typeof this.object.name == 'string') return `<${this.object.name}${this.forceIndefiniteArticle ? '!' : ''}${this.form != '' ? ' as ' + this.form : ''}>`;
        return `<${this.object.name.value}${this.forceIndefiniteArticle ? '!' : ''}${this.form != '' ? ' as ' + this.form : ''}>`;
    }

    equals(x: Signifier) {
        return this.object === x.object && this.form == x.form 
            && this.forceIndefiniteArticle == x.forceIndefiniteArticle;
    }

    clone(): Signifier {
        let sig = new Signifier(this.object, this.form);
        sig.forceIndefiniteArticle = this.forceIndefiniteArticle;
        return sig;
    }
}

export class VerbElement {
    type = 'verb' as const;
    isSPSeparable = false;
    isDurative = false;
    isStative = false;
    isPerfective = false;
    displayForm: string = '';

    constructor(public form: string, display: string = '') {
        if (form.endsWith('着')) this.isStative = true;
        this.displayForm = display;
    }

    durative() {
        this.isDurative = true;
        return this;
    }

    stative() {
        this.isStative = true;
        return this;
    }

    perfective() {
        this.isPerfective = true;
        return this;
    }

    equals(x: VerbElement) {
        return this.form == x.form && this.isSPSeparable == x.isSPSeparable
            && this.isStative == x.isStative && this.isPerfective == x.isPerfective;
    }

    clone(): VerbElement {
        let result = new VerbElement(this.form);
        result.isSPSeparable = this.isSPSeparable;
        result.isStative = this.isStative;
        result.isPerfective = this.isPerfective;
        return result;
    }
}

export type SyntaxElement = PlainText | Signifier | VerbElement | SyntaxNode;

const _N1 = {} as {
    [k in typeof SYNTAX_NODE_TYPES[number][1]]: 
        (...x: (SyntaxElement | Referent)[]) => SyntaxNode};
for (let t of SYNTAX_NODE_TYPES)
    _N1[t[1]] = (...x) => ({type: t[0], children: _processElementOrReferent(x)});

const _N2 = {
    V: (form: string) => new VerbElement(form),
    // helpers
    SVO: (s: SyntaxElement | Referent, v: string | VerbElement, 
            o?: SyntaxElement | Referent, ...rest: SyntaxElement[]) => 
        [_N1.S(s), _N1.P(
            typeof v == 'string' ? _N2.V(v) : v, 
            ...(o === undefined ? [] : [_N1.O(o)]), ...rest)],
    SP: (s: SyntaxElement | Referent, ...p: (SyntaxElement | Referent)[]) => [_N1.S(s), _N1.P(...p)],
    text: (...elem: SentenceContent) => [N.TEXT(...elem)]
}

export const N = {..._N1, ..._N2};

export const NH = {
    SVO: (type: HistoryEntryType, s: Referent, v: string | VerbElement, 
        o?: SyntaxElement | Referent, ...rest: SyntaxElement[]) => 
            new HistoryEntry(s, type, new Sentence(_N2.SVO(s, v, o, ...rest)), ''),
    SP: (type: HistoryEntryType, s: Referent, ...p: (SyntaxElement | Referent)[]) => 
            new HistoryEntry(s, type, new Sentence(_N2.SP(s, ...p)), ''),
}

const _SP1 = {} as {
    [k in typeof SYNTAX_NODE_TYPES[number][1]]: (name?: string) => SyntaxPattern};
for (let t of SYNTAX_NODE_TYPES)
    _SP1[t[1]] = (name: string = '') => new SyntaxPattern(t[0], name);

const _SP2 = {
    V: (name: string = '') => new SyntaxPattern('verb', name),
    group: (name: string = '', ...x: PatternOrText[]) => 
        new SyntaxPattern('grouped', name).with(...x),
    or: (name: string = '', ...x: PatternOrText[]) => 
        new SyntaxPattern('or', name).with(...x),
    any: (name: string = '') => new SyntaxPattern('any', name),
    signifier: (name: string = '') => new SyntaxPattern('signifier', name)
}

export const SP = {..._SP1, ..._SP2};

export enum Pronoun {
    You, I, 
    // Neutral, 
    She, He, It, 
    // ADV_TODO: TheyAnimate, 
    TheyInanimate
}

export const PronounList = [Pronoun.You, Pronoun.I, Pronoun.She, Pronoun.He, Pronoun.It, Pronoun.TheyInanimate] as const;
export const ProtectedPronouns = [Pronoun.I, Pronoun.You];
export const PluralPronouns = [Pronoun.TheyInanimate] as const;
export const SingularPronouns = 
    [Pronoun.I, Pronoun.You, Pronoun.She, Pronoun.He, Pronoun.It] as const;

export const PronounNames = {
    [Pronoun.You]: '你',
    [Pronoun.I]: '我',
    //[Pronoun.Neutral]: 'ta',
    [Pronoun.She]: '她',
    [Pronoun.He]: '他',
    [Pronoun.It]: '它',
    [Pronoun.TheyInanimate]: '它们',
    //[Pronoun.TheyAnimate]: '他们'

    '你': Pronoun.You,
    '我': Pronoun.I,
    '她': Pronoun.She,
    '他': Pronoun.He,
    '它': Pronoun.It,
    '它们': Pronoun.TheyInanimate
} as const;

export enum SentenceFlags {
    Unfinished, 
}

export type SentenceContent = (SyntaxElement | Referent)[];

export class Sentence {
    elements: SyntaxElement[];
    flags = new Set<SentenceFlags>;
    /** Same group name for consecutive sentences means that their order doesn't matter */
    group = '';

    constructor(elements: SentenceContent) {
        this.elements = _processElementOrReferent(elements);
    }

    cloneMeta(): Sentence {
        let result = new Sentence([]);
        result.flags = new Set(this.flags);
        result.group = this.group;
        return result;
    }

    clone(): Sentence {
        let result = this.cloneMeta();
        result.elements = this.elements.map((x) => NUtils.cloneElement(x));
        return result;
    }

}

function _processElementOrReferent(elements: SentenceContent) {
    return elements.map((x) => {
        if (x instanceof Agent || x instanceof AObject) 
            return new Signifier(x);
        return x;
    });
}

type PatternOrText = SyntaxPattern | string;
export type MatchResult = {
    pos: number, length: number, 
    parentNode: SyntaxNode | null, 
    capture: Map<string, SyntaxElement[]>
};

export class SyntaxPattern {
    isRepeating = false;
    isOptional = false;
    children: SyntaxPattern[] = [];
    stringValue = '';

    #nexts: SyntaxPattern[] = [];
    #optionalNext?: SyntaxPattern;
    #isFinal = false;

    constructor(public type: SyntaxElementType, public name: string = '') {}

    toString(): string {
        return `<${this.type}${this.name == '' ? '' : `:${this.name}`}>${this.isOptional ? '?' : ''}${this.isRepeating ? '+' : ''}${this.children.length > 0 ? `[${this.children.map((x) => x.toString()).join(' ')}]` : ''}`;
    }

    with(...children: PatternOrText[]) {
        this.children.push(...children.map(SyntaxPattern.#convertText));
        return this;
    }

    repeating() {
        this.isRepeating = true;
        return this;
    }

    optional() {
        this.isOptional = true;
        return this;
    }

    asterisk() {
        this.isOptional = true;
        this.isRepeating = true;
        return this;
    }

    get debugString(): string {
        return `[${this.name == '' ? '' : (this.name + ': ')}${this.type}${this.children.length > 0 ? ('{' + this.children.map((x) => x.debugString).join(', ') + '}') : ''}]`
    }

    #typeMatch(elem: SyntaxElement): boolean {
        if (this.type == 'any') return true;
        if (typeof elem == 'string')
            return this.type == 'plainText' && 
                (this.stringValue.length == 0 || elem == this.stringValue);
        return elem.type == this.type;
    }

    #matchOne(elem: SyntaxElement, map: Map<string, SyntaxElement[]>): number {
        // ADV_FIXME: currently named matched content of 'grouped' and 'or' nodes does
        // not appear in the returned map
        if (['grouped', 'or'].includes(this.type)) return 0;
        if (!this.#typeMatch(elem)) return -1;
        if (this.children.length > 0 && NUtils.isNode(elem)) {
            const newMap = SyntaxPattern.match(this.children, elem.children);
            if (!newMap) return -1;
            newMap.forEach((v, k) => {
                if (map.has(k)) map.get(k)?.push(...v);
                else map.set(k, v)
            });
        }
        if (this.name.length == 0) return 1;
        if (!map.has(this.name))
            map.set(this.name, []);
        map.get(this.name)?.push(elem)
        return 1;
    }

    #assignNexts(thisNext?: SyntaxPattern) {
        this.#optionalNext = thisNext;
        this.#isFinal = false;

        if (this.type == 'or' && this.children.length > 0) {
            this.#nexts = this.children;
            for (let child of this.children)
                child.#assignNexts(thisNext);
            if (this.isRepeating) this.#nexts.push(this);
        } else if (this.type == 'grouped' && this.children.length > 0) {
            this.#nexts = [this.children[0]];
            for (let i = 0; i < this.children.length - 1; i++)
                this.children[i].#assignNexts(this.children[i+1]);
            let lastChild = this.children[this.children.length - 1];
            lastChild.#assignNexts(thisNext);
            if (this.isRepeating) lastChild.#nexts.push(this);
        } else {
            this.#nexts = thisNext ? [thisNext] : [];
            if (this.isRepeating) this.#nexts.push(this);
            this.#isFinal = !thisNext;
        }
    }

    static #convertText(pot: PatternOrText) {
        let pat: SyntaxPattern;
        if (typeof pot == 'string') {
            pat = new SyntaxPattern('plainText');
            pat.stringValue = pot;
        } else pat = pot;
        return pat;
    }

    static #createGraph(pattern: SyntaxPattern[]) {
        Debug.assert(pattern.length > 0);
        for (let i = 0; i < pattern.length - 1; i++)
            pattern[i].#assignNexts(pattern[i+1]);
        pattern[pattern.length - 1].#assignNexts(undefined);
    }

    static #match(start: SyntaxPattern, content: SentenceContent, checkEnd: boolean)
            : [number, Map<string, SyntaxElement[]> | null] {

        const seq: SyntaxElement[] = _processElementOrReferent(content);
        const createState = (p: number, m: Map<string, SyntaxElement[]>, 
            n: SyntaxPattern) => {
                // console.log('creating state:', n.toString(), '@', p);
                return {pos: p, map: m, node: n}
            };
        type State = ReturnType<typeof createState>;

        const queue: State[] = [createState(0, new Map, start)];
        let currentState: State | undefined;
        // loop until queue is empty
        while (currentState = queue.shift()) {
            // console.log(currentState.node.toString(), '@', currentState.pos);
            if (currentState.pos >= seq.length) {
                if (currentState.node.isOptional) {
                    if (currentState.node.#isFinal)
                        return [currentState.pos, currentState.map];
                    for (const next of currentState.node.#nexts)
                        if (next != currentState.node) queue.push(
                            createState(currentState.pos, currentState.map, next));
                } else {
                    // console.log('failing: out of range');
                }
                continue;
            }
            // try matching this pattern node at this position
            const map = new Map(currentState.map);
            const ret = currentState.node.#matchOne(seq[currentState.pos], map);
            // only fail when not optional
            if (ret < 0 && !currentState.node.isOptional) {
                // console.log('failing: not a match');
                continue;
            }
            const nextPos = currentState.pos + Math.max(ret, 0);
            // check if the whole pattern succeeds
            if (currentState.node.#isFinal && (!checkEnd || nextPos == seq.length))
                return [nextPos, map];
            // otherwise, register next states
            for (const next of currentState.node.#nexts)
                queue.push(createState(nextPos, map, next));
            // if both ok and optional, add one special next state
            if (currentState.node.isOptional && currentState.node.#optionalNext)
                queue.push(createState(
                    currentState.pos, currentState.map, 
                    currentState.node.#optionalNext));
        }
        // match failed
        return [0, null];
    }

    /**
     * Validates a sequence against a given pattern.
     * @returns A `Map` of captured elements if the whole sequence matches the pattern, or `null` if it doesn't.
     */
    static match(pots: PatternOrText[], seq: SentenceContent) {
        // console.log('matching [', pots.map((x) => x.toString()).join(' '), '] against', ...seq.map((x) => NUtils.debugPrintElement(x)));
        const pattern = [...pots.map(SyntaxPattern.#convertText)];
        this.#createGraph(pattern);
        return this.#match(pattern[0], seq, true)[1];
    }

    /**
     * Attempt to find parts in a sequence that matches a given pattern, then optionally replace them using a given function. This function applies recursively; if a visited SyntaxNode in the sequence is not a match, its children will be searched.
     * @param maxNumber Find at most this many matches, e.g. set to 1 to find only the first; -1 is unlimited
     */
    static matchPart(
        pots: PatternOrText[], content: SentenceContent, maxNumber = -1, 
        replace?: (res: MatchResult) => SentenceContent): MatchResult[]
    {
        let nMatched = 0;
        function _match(
            start: SyntaxPattern, seq: SyntaxElement[], parent: SyntaxNode | null)
        {
            if (maxNumber > 0 && nMatched > maxNumber) return;
            let i = 0;
            while (i < seq.length) {
                let ret = SyntaxPattern.#match(start, seq.slice(i), false);
                if (ret[1]) {
                    const result = { pos: i, length: ret[0], 
                        parentNode: parent, capture: ret[1] };
                    results.push(result);
                    nMatched++;
                    if (replace) {
                        const replacement = _processElementOrReferent(replace(result));
                        const orig = seq.slice(i, i + ret[0]);
                        console.log('replaced', orig.map(NUtils.debugPrintElement), 'to', replacement.map(NUtils.debugPrintElement));
                        const rest = seq.slice(i + ret[0]);
                        seq.length = i;
                        seq.push(...replacement, ...rest);
                        i += replacement.length;
                    } else
                        i += Math.max(ret[0], 1);
                    if (maxNumber > 0 && nMatched >= maxNumber) return;
                } else {
                    const elem = seq[i];
                    if (NUtils.isNode(elem) && elem.children.length > 0)
                        _match(start, elem.children, elem);
                    i++;
                }
            }
        }

        const seq = _processElementOrReferent(content);
        const results: MatchResult[] = [];
        const pattern = [...pots.map(SyntaxPattern.#convertText)];
        this.#createGraph(pattern);
        _match(pattern[0], seq, null);
        return results;
    }
}

export class HistoryEntry {
    startTime?: number;
    endTime?: number;
    interruptedForm?: Sentence;
    group: string = '';
    implicit = false;
    interrupted = false;

    get isDurative() {
        return !(this.content instanceof Sentence);
    }

    // ADV_TODO: detailling affected areas
    constructor(
        public sender: Referent | undefined,
        public type: HistoryEntryType,
        public content: DurativeAspects | Sentence,
        public debugString: string) 
    {
        Debug.register(this, 'HistoryEntry should be submitted');
    }

    clone() {
        const obj = new HistoryEntry(
            this.sender, this.type, this.content, this.debugString);
        Object.assign(obj, this);
        return obj;
    }

    commence(parent?: Agent | AZone, _debug = true) {
        Debug.assert(this.startTime === undefined);
        this.startTime = World.schedule.absoluteTime;
        if (_debug) console.log('commenced:', this);
        if (this.sender && !parent) parent = this.sender instanceof Agent 
                ? this.sender : this.sender.parentZone ?? undefined;
        if (parent instanceof Agent)
            parent.memory.pushEntry(this);
        else
            parent?.history.pushEntry(this);
        return this;
    }

    cease() {
        Debug.assert(this.startTime !== undefined
            && this.endTime === undefined
            && this.startTime <= World.schedule.absoluteTime);
        this.endTime = World.schedule.absoluteTime;
        return this;
    }

    interrupt() {
        Debug.assert(this.startTime !== undefined
            && this.startTime <= World.schedule.absoluteTime
            && this.endTime == undefined);
        console.log('interrupting', this);
        this.interrupted = true;
        this.endTime = World.schedule.absoluteTime;
        return this;
    }

    submitMomentary() {
        this.commence(undefined, false);
        this.endTime = World.schedule.absoluteTime;
        return this;
    }

    markGroup(value: string) {
        this.group = value;
        return this;
    }

    markImplicit(value = true) {
        this.implicit = value;
        return this;
    }
}

Debug.makeCustomViewerClass(HistoryEntry,
    (obj) => {
        const getT = (s?: number) => s === undefined 
            ? ['span', {style: 'color: gray'},'...'] as const 
            : U.timeString(s);
        return ['span', 
            ['span', {style: 'color: darkblue'}, 'entry '], 
            '(', getT(obj.startTime), ' - ', getT(obj.endTime), ') ',
            ['span', {style: 'color: gray'}, 
                (obj.implicit ? 'imp ' : '') + 
                (obj.isDurative ? 'dur ' : ''),
                ...(obj.sender ? 
                    ['from ', ['object', {object: obj.sender}]] as const : [])]
            ] as const;
    },
    (obj) => ['object', {object: obj.content}]);

Debug.makeCustomViewer<SyntaxNode>(
    (obj) => NUtils.isNode(obj),
    (obj) => {
        let str = obj.children.map((x) => NUtils.debugPrintElementSimple(x)).join(' ');
        return ['span', {style: 'text-overflow: ellipsis'}, ['span', {style: 'color: gray; padding: 0 3px'}, obj.type], str];
    },
    (obj) => {
        if (obj.children.length == 1 && typeof obj.children[0] == 'string')
            return null;
        let inner = obj.children.map<DevtoolsFormatterItem>(
            (x) => ['li', {style: 'line-height: 1.2'},
                ['span', {style: 'padding: 0 1ch; color: darkblue'}, '*'],
                ['span', typeof x == 'string' ? x : ['object', {object: x}]]]);
        return ['ol', {style: 'list-style: none; padding: 0; margin: 0'}, ...inner];
    });

Debug.makeCustomViewerClass(Signifier,
    (obj) => {
        let objname = obj.object instanceof Agent 
            ? obj.object.name : obj.object.name.value;
        return ['span',
            ['span', {style: 'color: gray; padding-right: 4px'}, 'signifier['], 
            objname,
            ['span', {style: 'color: gray; padding-left: 4px'}, 
                (obj.form.length > 0 ? `as ${obj.form}` : '') + '] ' +
                (obj.forceIndefiniteArticle ? 'def' : '')]];
    },
    (obj) => ['object', {object: obj.object}]);

Debug.makeCustomViewerClass(VerbElement,
    (obj) => ['span',
        ['span', {style: 'color: gray; padding-right: 4px'}, 'verb['], 
        obj.form,
        ['span', {style: 'color: gray; padding-left: 4px'}, 
            (obj.displayForm.length > 0 ? `as ${obj.displayForm}` : '') + '] ' +
            (obj.isStative ? 'sta ' : '') +
            (obj.isDurative ? 'dur ' : '') + 
            (obj.isPerfective ? 'perf' : '')]]);

Debug.makeCustomStringViewClass(Sentence,
    (s) => {
        let str = '';
        if (s.flags.size > 0)
            str += [...s.flags].map((x) => SentenceFlags[x]).join('|') + ':';
        str += s.elements.map(NUtils.debugPrintElementSimple).join(' ');
        return [...(s.group != '' ? [
                ['span', {style: 'color: white; background-color: gray; padding: 0px 4px; round; border-radius: 3px; margin-right: 4px'}, s.group]
            ] as const : []), 
            ['span', {style: 'color: gray; padding: 2px'}, '['],
            ['span', str],
            ['span', {style: 'color: gray; padding: 2px'}, ']']];
    },
    (s) => s.elements);

export type HistoryEntryType = 'action' | 'thought' | 'meta' | 'annotation';

export type DurativeAspects = {
    perfective?: Sentence;
    inchoative?: Sentence;
    cessative?: Sentence;
    progressive?: Sentence;
};

export const NUtils = {
    cloneElement(x: SyntaxElement): SyntaxElement {
        if (typeof x == 'string') return x;
        if (x instanceof Signifier || x instanceof VerbElement) return x.clone();
        if (NUtils.isNode(x)) {
            return {
                type: x.type,
                children: x.children.map((child) => NUtils.cloneElement(child))
            };
        }
        Debug.assert(false);
    },
    cloneElements(x: SyntaxElement[]): SyntaxElement[] {
        return x.map((elem) => NUtils.cloneElement(elem));
    },

    isNode(x: SyntaxElement): x is SyntaxNode {
        return (x as SyntaxNode).type != undefined &&
            Array.isArray((x as SyntaxNode).children);
    },

    combineElements(...elems: SyntaxElement[]): SyntaxNode {
        Debug.assert(elems.length > 1);
        let list = elems.reduce((l: SyntaxElement[], c) => {
            if (NUtils.isNode(c) && c.type == 'nounPhraseList')
                l.push(...c.children);
            else l.push(c);
            return l;
        }, []);
        return N.LIST(...list);
    },

    debugPrintElementSimple(elem: SyntaxElement): string {
        if (typeof elem == 'string') return elem;
        if (NUtils.isNode(elem)) {
            if (elem.type == 'coordinatingPredicatives') return `[${elem.type}]:\n - ${elem.children.map((x) => NUtils.debugPrintElementSimple(x)).join('\n - ')}`;
            return elem.children.map((x) => NUtils.debugPrintElementSimple(x)).join(' ');
        }
        if (elem instanceof Signifier) return elem.debugString;
        if (elem instanceof VerbElement) return elem.form;
        Debug.assert(false);
    },

    debugPrintElement(elem: SyntaxElement): string {
        if (typeof elem == 'string') return elem;
        if (NUtils.isNode(elem)) {
            if (elem.type == 'nounPhraseList') return `list:{${elem.children.map((x) => NUtils.debugPrintElement(x)).join(', ')}}`;
            if (elem.type == 'coordinatingPredicatives') return `[${elem.type}]:\n - ${elem.children.map((x) => NUtils.debugPrintElement(x)).join('\n - ')}`;
            return `[${elem.type}](${elem.children.map((x) => NUtils.debugPrintElement(x)).join(' ')})`;
        }
        if (elem instanceof Signifier) return elem.debugString;
        if (elem instanceof VerbElement) return `V:${elem.form}`;
        Debug.assert(false);
    },

    equals(x: SyntaxElement, y: SyntaxElement) {
        if (!x || !y) return false;
        if (typeof x == 'string')
            return typeof y == 'string' && x == y;
        if (x instanceof Signifier)
            return y instanceof Signifier && x.equals(y);
        if (x instanceof VerbElement)
            return y instanceof VerbElement && x.equals(y);

        if (!NUtils.isNode(y)) return false;
        return x.type == y.type && NUtils.allEquals(x.children, y.children);
    },
    /**
     * Always returns false for undefined values.
     */
    allEquals(x?: SyntaxElement[], y?: SyntaxElement[]) {
        if (!x || !y) return false;
        if (x.length != y.length) return false;
        for (let i = 0; i < x.length; i++)
            if (!NUtils.equals(x[i], y[i])) return false;
        return true;
    }
};
